/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2012 Samuel Marshall.
*/
package com.leafdigital.ui;
import java.awt.*;
import java.awt.dnd.DragSource;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import com.leafdigital.ui.api.*;
import com.leafdigital.ui.api.TreeBox.*;
import leafchat.core.api.BugException;
/** Combo box */
public class TreeBoxImp extends JScrollPane implements TreeSelectionListener
{
private JTree t;
private TreeBox.Handler th=null;
private int disableSelectionEvents;
private class TreeBoxModel implements TreeModel
{
private java.util.List<TreeModelListener> listeners =
new LinkedList<TreeModelListener>();
@Override
public Object getRoot()
{
if(th==null)
return null;
else
return th.getRoot();
}
@Override
public Object getChild(Object oParent,int iIndex)
{
return ((TreeBox.Item)oParent).getChildren()[iIndex];
}
@Override
public int getChildCount(Object oParent)
{
if(oParent==null)
return 0;
else
return ((TreeBox.Item)oParent).getChildren().length;
}
@Override
public boolean isLeaf(Object oParent)
{
return ((TreeBox.Item)oParent).isLeaf();
}
@Override
public void valueForPathChanged(TreePath arg0,Object arg1)
{
// Ignore, not editable
throw new Error("Can't edit trees");
}
@Override
public int getIndexOfChild(Object oParent,Object oChild)
{
if(oParent==null || oChild==null) return -1;
TreeBox.Item[] aiSearch=((TreeBox.Item)oParent).getChildren();
for(int i=0;i<aiSearch.length;i++)
{
if(aiSearch[i]==oChild) return i;
}
return -1;
}
@Override
public void addTreeModelListener(TreeModelListener tml)
{
synchronized(listeners)
{
listeners.add(tml);
}
}
@Override
public void removeTreeModelListener(TreeModelListener tml)
{
synchronized(listeners)
{
listeners.remove(tml);
}
}
private void update()
{
try
{
disableSelectionEvents++;
synchronized(listeners)
{
for(TreeModelListener tml : listeners)
{
tml.treeStructureChanged(new TreeModelEvent(
TreeBoxImp.this,new Object[] {getRoot()}
));
}
}
}
finally
{
disableSelectionEvents--;
}
// Selection might have changed
valueChanged(null);
}
TreePath find(Item i)
{
if(th==null) return null;
Item iRoot=th.getRoot();
TreePath tp=new TreePath(iRoot);
return recursiveFind(i,iRoot,tp);
}
private TreePath recursiveFind(Item iFind,Item iCurrent,TreePath tpCurrent)
{
if(iFind==iCurrent) return tpCurrent;
Item[] ai=iCurrent.getChildren();
for(int i=0;i<ai.length;i++)
{
TreePath tpResult=recursiveFind(iFind,ai[i],tpCurrent.pathByAddingChild(ai[i]));
if(tpResult!=null) return tpResult;
}
return null;
}
}
class TreeBoxRenderer extends DefaultTreeCellRenderer
{
@Override
public Component getTreeCellRendererComponent(JTree t,Object o,boolean bSelected,boolean bExpanded,
boolean bLeaf,int iRow,boolean bHasFocus)
{
boolean dragging=((DragPaintTree)t).isDragging(iRow);
super.getTreeCellRendererComponent(t,o,!dragging && bSelected,bExpanded,bLeaf,iRow,bHasFocus);
if(dragging)
{
Color fg=getForeground();
setForeground(new Color(fg.getRed(),fg.getGreen(),fg.getBlue(),128));
}
if(o==t.getModel())
{
setText("[Root]");
}
else
{
TreeBox.Item ti=(TreeBox.Item)o;
setText(ti.getText());
Image icon = ti.getIcon();
if(icon != null)
{
setIcon(new ImageIcon(icon));
}
}
return this;
}
}
TreeBoxImp()
{
setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS);
DragHandler dh=new DragHandler();
t=new DragPaintTree(dh);
t.setModel(new TreeBoxModel());
t.setCellRenderer(new TreeBoxRenderer());
t.addTreeSelectionListener(this);
t.addMouseListener(dh);
t.addMouseMotionListener(dh);
setViewportView(t);
}
private final static int POSITION_ABOVE=1,POSITION_BELOW=2,POSITION_ON=3;
private static class DragPaintTree extends JTree
{
private DragHandler dh;
DragPaintTree(DragHandler dh)
{
this.dh=dh;
}
@Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
dh.paintHighlight(g);
}
boolean isDragging(int row)
{
return dh.dragging!=null && dh.dragging.row==row;
}
}
private class DragHandler implements MouseListener,MouseMotionListener
{
PositionDetails dragging,highlight;
private Point getPoint(MouseEvent e)
{
Point
scrollPanePos=TreeBoxImp.this.getLocationOnScreen(),
treePos=t.getLocationOnScreen();
return new Point(e.getX()+scrollPanePos.x-treePos.x,
e.getY()+scrollPanePos.y-treePos.y);
}
private void setCursor(Cursor c)
{
t. setCursor(c);
}
private void paintHighlight(Graphics g)
{
if(dragging==null) return;
Color foreground=t.getForeground();
if(dragging.row!=-1)
{
Rectangle start=t.getRowBounds(dragging.row);
g.setColor(new Color(
foreground.getRed(),foreground.getGreen(),foreground.getBlue(),128));
g.drawRect(start.x,start.y,start.width,start.height);
}
if(highlight!=null && highlight.row!=-1)
{
Rectangle target=t.getRowBounds(highlight.row);
g.setColor(foreground);
switch(highlight.position)
{
case POSITION_ON:
g.drawRect(target.x,target.y,t.getWidth()-target.x,target.height);
break;
case POSITION_ABOVE:
g.drawLine(target.x,target.y-1,t.getWidth(),target.y-1);
break;
case POSITION_BELOW:
g.drawLine(target.x,target.y+target.height-1,t.getWidth(),target.y+target.height-1);
break;
}
}
}
private class PositionDetails
{
Item i;
int row;
int position;
Item parent;
int parentPos;
}
private PositionDetails getPositionDetails(MouseEvent e,boolean loose)
{
Point p=getPoint(e);
int row=loose ? t.getClosestRowForLocation(p.x,p.y) : t.getRowForLocation(p.x,p.y);
if(row==-1) return null;
PositionDetails pd=new PositionDetails();
TreePath tp=t.getPathForRow(row);
pd.i=(Item)tp.getLastPathComponent();
pd.row=row;
if(pd.i.getParent()==null) return null;
Rectangle bounds=t.getRowBounds(row);
if(!pd.i.isLeaf() &&
p.y>bounds.y+3 && p.y<bounds.y+bounds.height-3)
{
pd.position=POSITION_ON;
pd.parent=pd.i;
pd.parentPos=pd.parent.getChildren().length;
}
else if(p.y<(bounds.y+bounds.height/2))
{
pd.position=POSITION_ABOVE;
pd.parent=pd.i.getParent();
Item[] siblings=pd.parent.getChildren();
for(int i=0;i<siblings.length;i++)
{
if(siblings[i]==pd.i)
{
pd.parentPos=i;
}
}
}
else
{
pd.position=POSITION_BELOW;
pd.parent=pd.i.getParent();
Item[] siblings=pd.parent.getChildren();
for(int i=0;i<siblings.length;i++)
{
if(siblings[i]==pd.i)
{
pd.parentPos=i+1;
}
}
}
return pd;
}
@Override
public void mousePressed(MouseEvent e)
{
PositionDetails pd=getPositionDetails(e,false);
if(pd!=null && (th instanceof DragSingleSelectionHandler) &&
((DragSingleSelectionHandler)th).canDrag(pd.i))
{
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
dragging=pd;
t.repaint();
}
}
private boolean sameItem(PositionDetails pd)
{
if(pd.i==dragging.i) return true; // Same item
if(pd.parent!=dragging.parent) return false; // Different parent
// Same parent, but are we looking at same pos?
for(int pos=0;pos<dragging.i.getParent().getChildren().length;pos++)
{
if(dragging.i.getParent().getChildren()[pos]==dragging.i)
{
return pos==pd.parentPos;
}
}
return false; // wtf?
}
@Override
public void mouseDragged(MouseEvent e)
{
if(dragging==null) return;
highlight=getPositionDetails(e,false);
if(highlight!=null && !sameItem(highlight) &&
((DragSingleSelectionHandler)th).canDragTo(
dragging.i,highlight.parent,getTargetPos()))
{
t.repaint();
setCursor(DragSource.DefaultMoveDrop);
}
else
{
highlight=null;
t.repaint();
setCursor(DragSource.DefaultMoveNoDrop);
}
}
private int getTargetPos()
{
// If the items are in the same container, we adjust the index to assume
// that the original item had been removed
int targetPos=highlight.parentPos;
if(dragging.i.getParent()==highlight.parent)
{
for(int i=0;i<targetPos;i++)
{
if(dragging.i.getParent().getChildren()[i]==dragging.i)
{
targetPos--;
break;
}
}
}
return targetPos;
}
@Override
public void mouseReleased(MouseEvent e)
{
if(dragging==null) return;
if(highlight!=null)
{
((DragSingleSelectionHandler)th).dragTo(
dragging.i,highlight.parent,getTargetPos());
}
dragging=null;
highlight=null;
t.repaint();
setCursor(
Cursor.getDefaultCursor());
}
@Override
public void mouseMoved(MouseEvent e) {}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}
/** @return Tree interface */
public TreeBox getInterface() { return tbInterface; }
/** Tree interface */
TreeBox tbInterface=new TreeBoxInterface();
/** Class implementing tree box interface */
class TreeBoxInterface extends BasicWidget implements TreeBox, InternalWidget
{
private int iWidth=-1,height=-1;
@Override
public int getContentType() { return CONTENT_NONE; }
@Override
public void addXMLChild(String sSlotName, Widget wChild)
{
throw new BugException("Trees cannot contain children");
}
@Override
public JComponent getJComponent()
{
return TreeBoxImp.this;
}
@Override
public int getPreferredWidth()
{
if(iWidth!=-1)
return iWidth;
else
return getPreferredSize().width;
}
@Override
public int getPreferredHeight(int iWidth)
{
if(height!=-1)
return height;
else
return getPreferredSize().height;
}
@Override
public void setHandler(Handler th)
{
TreeBoxImp.this.th=th;
t.setRootVisible(th.isRootDisplayed());
if(th instanceof MultiSelectionHandler)
{
t.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
}
else
{
t.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
}
try
{
disableSelectionEvents++;
update();
}
finally
{
disableSelectionEvents--;
}
}
@Override
public void update()
{
((TreeBoxModel)t.getModel()).update();
}
@Override
public void setWidth(int iWidth)
{
this.iWidth=iWidth;
}
@Override
public void setHeight(int height)
{
this.height=height;
}
@Override
public void select(Item i)
{
try
{
disableSelectionEvents++;
t.setSelectionPath(((TreeBoxModel)t.getModel()).find(i));
}
finally
{
disableSelectionEvents--;
}
}
@Override
public void select(Item[] ai)
{
try
{
disableSelectionEvents++;
TreePath[] atp=new TreePath[ai.length];
for(int i=0;i<atp.length;i++)
{
atp[i]=((TreeBoxModel)t.getModel()).find(ai[i]);
}
t.setSelectionPaths(atp);
}
finally
{
disableSelectionEvents--;
}
}
}
private TreeBox.Item lastSingleSelection;
private TreeBox.Item[] lastMultiSelection;
@Override
public void valueChanged(TreeSelectionEvent e)
{
if(disableSelectionEvents>0) return;
if(th==null) return;
if(th instanceof SingleSelectionHandler)
{
SingleSelectionHandler ssh=(SingleSelectionHandler)th;
TreePath tp=t.getSelectionPath();
TreeBox.Item newSelection;
if(tp==null)
newSelection=null;
else
newSelection=(TreeBox.Item)tp.getLastPathComponent();
if(newSelection!=lastSingleSelection)
{
lastSingleSelection=newSelection;
ssh.selected(newSelection);
}
}
else if(th instanceof MultiSelectionHandler)
{
TreePath[] atp=t.getSelectionPaths();
if(atp==null) atp=new TreePath[0];
Item[] ai=new Item[atp.length];
for(int i=0;i<ai.length;i++)
{
ai[i]=(TreeBox.Item)atp[i].getLastPathComponent();
}
if(lastMultiSelection==null || !Arrays.equals(ai,lastMultiSelection))
{
lastMultiSelection=ai;
((MultiSelectionHandler)th).selected(ai);
}
}
}
}